package cn.edu.tju.fctl.util;

import cn.edu.tju.fctl.entities.LeaveInfo;
import cn.edu.tju.fctl.entities.LeaveTime;
import cn.edu.tju.fctl.entities.Mark;

import java.sql.Time;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Created by H on 2015/12/26.
 */
public class EvaluationUtil {

    private class TimePeriod{
        public static final int LEAVE = 1, MARK = 2, OUT = 3, NONE = 4;
        private Date startTime,endTime;

        private int type = 0;
        private String leaveType = "";

        public TimePeriod(Date startTime, Date endTime){
            this.startTime = startTime;
            this.endTime = endTime;
        }

        public TimePeriod(Date startTime, Date endTime, int type){
            this.startTime = startTime;
            this.endTime = endTime;
            this.type = type;
        }

        public TimePeriod(Date startTime, Date endTime, int type, String leaveType){
            this.startTime = startTime;
            this.endTime = endTime;
            this.type = type;
            this.leaveType = leaveType;
        }

        public void setStartTime(Date startTime) {
            this.startTime = startTime;
        }

        public void setEndTime(Date endTime) {
            this.endTime = endTime;
        }

        public int getType() {
            return type;
        }

        public void setType(int type) {
            type = type;
        }

        public String getLeaveType() {
            return leaveType;
        }

        public void setLeaveType(String leaveType) {
            this.leaveType = leaveType;
        }

        public Date getStartTime() {
            return startTime;
        }

        public Date getEndTime() {
            return endTime;
        }

    }

    private List<TimePeriod> workTimes;
    private List<Integer> restWeekDays;

    public EvaluationUtil(){
        //work time: 8:00 - 12:00, 13:30 - 17:30
        workTimes = new ArrayList<TimePeriod>();
        workTimes.add(new TimePeriod(new Date(0,1,1,8,0,0), new Date(0,1,1,12,0,0)));
        workTimes.add(new TimePeriod(new Date(0,1,1,13,30,0), new Date(0,1,1,17,30,0)));
        restWeekDays = new ArrayList<Integer>();
        restWeekDays.add(Calendar.SATURDAY);
        restWeekDays.add(Calendar.SUNDAY);
    }

    public static EvaluationUtil getInstance(){
        return new EvaluationUtil();
    }

    public Evaluation getEvaluation(int userId,List<LeaveInfo> listAllLeave, List<Mark> listMark, Date fromDate, Date toDate){
        //the periods of leaves, marks, outs can't overlap each other. If they overlap, leave > mark > out
        //leave, absence, overtime
        //format time axis
        List<TimePeriod> listTime = new ArrayList<TimePeriod>();
        for(LeaveInfo tempLeave:listAllLeave){
            if(tempLeave.getLeaveTimes() == null){
                System.out.println("getEvaluation: null leaveTimes.");
            }
            for(LeaveTime tempLeaveTime: tempLeave.getLeaveTimes()){
                if(tempLeave.getType().equals((VacationTypeUtil.OUT + ""))){
                    listTime.add(new TimePeriod(tempLeaveTime.getStartTime(), tempLeaveTime.getEndTime(), TimePeriod.OUT));
                }else{
                    listTime.add(new TimePeriod(tempLeaveTime.getStartTime(), tempLeaveTime.getEndTime(),
                            TimePeriod.LEAVE, tempLeave.getType()));
                }
            }
        }
        for(Mark tempMark: listMark){
            listTime.add(new TimePeriod(tempMark.getStartTime(), tempMark.getEndTime(), TimePeriod.MARK));
        }

        //sort time axis
        Collections.sort(listTime, new TPStartTimeComparator());
        listTime = dealWithOverlap(listTime, fromDate, toDate);

        return calculateHours(userId, fromDate, toDate, listTime);
    }

    //time axis is formatted, no overlap. axis according to work time to calculate hours.
    private Evaluation calculateHours(int userId, Date fromDate, Date toDate, List<TimePeriod> listTime) {
        Evaluation res = new Evaluation(userId, fromDate, toDate);
        double[] leaveHours = res.getLeaveHours();
        double outHour = 0, absentHour = 0, overHour = 0;
        Date current = getNextWorkTime(fromDate);
        double[] tempHours;
        for(TimePeriod tempTimePeroid: listTime){
            if(current.getTime() < tempTimePeroid.getStartTime().getTime()){
                //may absence, may nothing
                tempHours = getPeroidWorkHour(current,tempTimePeroid.getStartTime());
                absentHour += tempHours[0];
            }
            //deal with time period, may leave, mark, out
            tempHours = getPeroidWorkHour(tempTimePeroid.getStartTime(),tempTimePeroid.getEndTime());
            switch (tempTimePeroid.getType()){
                case TimePeriod.LEAVE:
                    leaveHours[Integer.parseInt(tempTimePeroid.getLeaveType()) - 1] += tempHours[0];
                    break;
                case TimePeriod.MARK:
                    overHour += tempHours[1];
                    break;
                case TimePeriod.OUT:
                    overHour += tempHours[1];
                    break;
                default:
                    break;
            }
            current = tempTimePeroid.getEndTime();
        }
        res.setLeaveHours(leaveHours);
        res.setOutHour(outHour);
        res.setAbsentHour(absentHour);
        res.setOverHour(overHour);
        return res;
    }

    //according to workTimes and current time, get next work time.
    //people won't work in weekends, but they have no holidays.
    private Date getNextWorkTime(Date current) {
        int next = -1,count = 0;
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for(TimePeriod workPeriod: workTimes){
            int flag = sdf.format(workPeriod.getStartTime()).compareTo(sdf.format(current));
            if(flag > 0 && next == -1){
                next = count;
            }
            ++count;
        }
        if(next == -1){//current is latter than the last period
            Calendar cal = Calendar.getInstance();
            try {
                cal.setTime(sdf2.parse(sdf1.format(current.getTime()) + " " + sdf.format(workTimes.get(0).getStartTime())));
            } catch (ParseException e) {
                e.printStackTrace();
                return null;
            }
            cal.add(Calendar.DATE,1);
            //only consider about weekends, no holidays. for now
            while(!isWorkTime(cal.getTime())){
                cal.add(Calendar.DATE,1);
            }
            return cal.getTime();
        }else{
            try {
                return sdf2.parse(sdf1.format(current) + " " + sdf.format(workTimes.get(next).getStartTime()));
            } catch (ParseException e) {
                e.printStackTrace();
                return null;
            }
        }
    }

    //if error occurs, will return null
    private Date getNextRestTime(Date current){
        if(!isWorkTime(current)){
            current = getNextWorkTime(current);
        }
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for(TimePeriod tp:workTimes){
            if(sdf.format(tp.getStartTime()).compareTo(sdf.format(current)) <= 0
                    && sdf.format(tp.getEndTime()).compareTo(sdf.format(current)) > 0){

                try {
                    return sdf2.parse(sdf1.format(current.getTime()) + " " + sdf.format(tp.getEndTime().getTime()));
                } catch (ParseException e) {
                    return null;
                }
            }
        }
        return null;
    }

    private boolean isWorkTime(Date current){
        Calendar cal = Calendar.getInstance();
        cal.setTime(current);
        if(restWeekDays.contains(cal.get(Calendar.DAY_OF_WEEK))){
            return false;
        }
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        for(TimePeriod tp:workTimes){
            if(sdf.format(tp.getStartTime()).compareTo(sdf.format(current)) <= 0
                    && sdf.format(tp.getEndTime()).compareTo(sdf.format(current)) > 0){
                return true;
            }
        }
        return false;
    }

    //res[0] is work duration, res[1] is rest duration
    private double[] getPeroidWorkHour(Date startTime, Date endTime){
        double[] res = {0,0};
        int flag = 0;
        Date tempTime,flagTime = startTime;
        while(flagTime.getTime() < endTime.getTime()){
            if(isWorkTime(flagTime)){
                tempTime = getNextRestTime(flagTime);
                flag = 0;
            }else{
                tempTime = getNextWorkTime(flagTime);
                flag = 1;
            }

            if(tempTime.getTime() > endTime.getTime()){
                res[flag] += (endTime.getTime() - flagTime.getTime())/1000/60/60;
            }else{
                res[flag] += (tempTime.getTime() - flagTime.getTime())/1000/60/60;
            }
            flagTime = tempTime;
        }
        return res;
    }

    //deal with overlap, and limit
    private List<TimePeriod> dealWithOverlap(List<TimePeriod> listTime, Date fromDate, Date toDate) {
        if(listTime.size() == 0)
            return listTime;
        if(listTime.size() >= 2){
            List<TimePeriod> tempRes = new ArrayList<TimePeriod>();
            List<TimePeriod> subListTime = new ArrayList<TimePeriod>();
            for(int i = 0;i < listTime.size();++i){
                subListTime = new ArrayList<TimePeriod>();
                subListTime.add(listTime.get(i));
                while(i < listTime.size() - 1
                        && listTime.get(i).getEndTime().getTime() > listTime.get(i + 1).getStartTime().getTime()){
                    //when overlap. current end time latter than next start time.
                    subListTime.add(listTime.get(++i));
                }
                //deal overlap, core code
                if(subListTime.size() > 1){
                    List<TimePeriod> listOverlap = new ArrayList<TimePeriod>();
                    listOverlap.add(subListTime.get(0));
                    subListTime.remove(0);
                    for(int j = 0;j < subListTime.size();++j){
                        TimePeriod tempTp = subListTime.get(j);
                        while(tempTp.getStartTime().getTime() < tempTp.getEndTime().getTime()){
                            int num = findFirstOverlap(listOverlap,tempTp);
                            if(num != -1){
                                //overlap occur
                                Collection<TimePeriod> collection = getPeriodCollection(listOverlap.get(num),tempTp);
                                if(listOverlap.get(num).getEndTime().getTime() >= tempTp.getEndTime().getTime()){
                                    //tempTp is used out
                                    tempTp.setStartTime(tempTp.getEndTime());
                                }else{
                                    tempTp.setStartTime(listOverlap.get(num).endTime);
                                    collection.remove(collection.size() - 1);
                                }
                                subListTime.remove(num);
                                subListTime.addAll(num,collection);
                            }else{
                                // not overlap any more
                                listOverlap.add(findFirstPosition(listOverlap,tempTp),tempTp);
                                //tempTp is used out too
                                tempTp.setStartTime(tempTp.getEndTime());
                            }
                        }
                    }
                }

                //dump
                for(TimePeriod tempTimePeriod: subListTime){
                    tempRes.add(tempTimePeriod);
                }
            }
            listTime = tempRes;
        }
        if(listTime.get(0).getStartTime().getTime() < fromDate.getTime()){
            listTime.get(0).setStartTime(fromDate);
        }
        if(listTime.get(listTime.size() - 1).getEndTime().getTime() > toDate.getTime()){
            listTime.get(listTime.size() - 1).setEndTime(toDate);
        }
        return listTime;
    }

    //two parameters must overlap each other.
    private List<TimePeriod> getPeriodCollection(TimePeriod tp0, TimePeriod tp1) {
        List<TimePeriod> res = new ArrayList<TimePeriod>();
        if(tp0.getStartTime().getTime() > tp1.getStartTime().getTime()){
            //if tp0 latter than tp1, switch them.
            TimePeriod tempTp = tp0;
            tp0 = tp1;
            tp1 = tempTp;
        }
        if(tp0.getStartTime().getTime() < tp1.getStartTime().getTime()){
            //first not overlapped period, if exists.
            res.add(new TimePeriod(tp0.getStartTime(),tp1.getStartTime(),tp0.getType(),tp0.getLeaveType()));
            tp0.setStartTime(tp1.getStartTime());
        }
        //overlapped part.
        if(tp0.getEndTime().getTime() > tp1.getEndTime().getTime()){
            TimePeriod tempTp = tp0;
            tp0 = tp1;
            tp1 = tempTp;
        }
        TimePeriod example = getPriorTimePeriod(tp0,tp1);
        res.add(new TimePeriod(tp0.getStartTime(),tp0.getEndTime(),example.getType(),example.getLeaveType()));
        tp1.setStartTime(tp0.getEndTime());

        //last not overlapped period, if exists.
        if(tp1.getStartTime().getTime() < tp1.getEndTime().getTime()){
            res.add(new TimePeriod(tp1.getStartTime(),tp1.getEndTime(),tp1.getType(),tp1.getLeaveType()));
        }
        return res;
    }

    //values of types of TimePeriod is just same as the priority for now. If it is changed, remember to fix here.
    private TimePeriod getPriorTimePeriod(TimePeriod tp0, TimePeriod tp1) {
        return (tp0.getType() < tp1.getType())?tp0:tp1;
    }

    //only consider start time.
    private int findFirstPosition(List<TimePeriod> listTimePeriod, TimePeriod timePeriod) {
        int res = 0;
        for(TimePeriod tempTp:listTimePeriod){
            if(tempTp.getStartTime().getTime() <= timePeriod.getStartTime().getTime()){
                ++res;
            }else{
                return res;
            }
        }
        return res;
    }

    //if none, return -1. only consider start time of timePeriod
    private int findFirstOverlap(List<TimePeriod> listOverlap, TimePeriod timePeriod) {
        int res = 0;
        for(TimePeriod tempTp:listOverlap){
            if(tempTp.getStartTime().getTime() <= timePeriod.getStartTime().getTime() &&
                    tempTp.getEndTime().getTime() > timePeriod.getStartTime().getTime()){
                return res;
            }
            ++res;
        }
        return -1;
    }

    //compare 2 TimePeriod by start time.
    static class TPStartTimeComparator implements Comparator {
        public int compare(Object object1, Object object2) {
            TimePeriod tp0 = (TimePeriod) object1;
            TimePeriod tp1 = (TimePeriod) object2;
            return tp0.getStartTime().compareTo(tp1.getStartTime());
        }
    }

}
